/* @flow */

import config from '../config'
import Watcher from '../observer/watcher'
import Dep, {pushTarget, popTarget} from '../observer/dep'
import {isUpdatingChildComponent} from './lifecycle'

import {
  set,
  del,
  observe,
  defineReactive,
  toggleObserving
} from '../observer/index'

import {
  warn,
  bind,
  noop,
  hasOwn,
  hyphenate,
  isReserved,
  handleError,
  nativeWatch,
  validateProp,
  isPlainObject,
  isServerRendering,
  isReservedAttribute,
  invokeWithErrorHandling
} from '../util/index'

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

// 代理函数，将key代理到target上
export function proxy(target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter() {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter(val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

/*
* 初始化数据
* 1. 数据响应式的入口：分别处理props，methods，data，computed，watch
* 2. 优先级：props，methods，data，computed对象中属性不能出现重复
*             computed中的key不能和props，data中的key重复，methods不影响
*
* */
export function initState(vm: Component) {
  vm._watchers = []
  const opts = vm.$options

  // 针对props对象，为每个props对象设置响应式，将其代理到vm实例上
  if (opts.props) initProps(vm, opts.props)

  // 针对methods对象，检验每个属性的值是否为函数，和props的属性对比进行判重，最后得到vm[key] = methods[key]
  if (opts.methods) initMethods(vm, opts.methods)

  /*
  * 针对data对象
  *   1. 判重，data对象中属性不能和props，methods对象上的属性相同
  *   2. 代理data对象上的属性到vm实例
  *   3. 为data对象上的数据设置相响应式
  *
  * */
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }

  /*
  *
  * 针对computed对象
  *   1. 为computed[key]创建watcher实例，默认是懒执行
  *   2. 代理computed[key]到vm实例
  *   3. 判重，computed中的key不能和data，props中的属性重复
  *
  * */
  if (opts.computed) initComputed(vm, opts.computed)

  /*
 *
 * 针对watch对象
 *   1. 处理computed对象
 *   2. 为每个watch.key创建watcher实例，key和watcher实例可能是一对多的关系
 *   3. 如果设置了immediate，则立即执行回调函数
 *
 * */
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

// 处理props对象，为props对象的每个属性设置响应式，并将其代理到vm实例上
// vm：当前实例
// propsOptions：当前实例props配置项
function initProps(vm: Component, propsOptions: Object) {
  // 储存props对象
  const propsData = vm.$options.propsData || {}

  const props = vm._props = {}

  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  // 缓存每个props的key
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // root instance props should be converted
  if (!isRoot) {
    toggleObserving(false)
  }

  // 遍历props对象配置
  for (const key in propsOptions) {
    keys.push(key)

    // 获取props[key]的默认值
    const value = validateProp(key, propsOptions, propsData, vm)
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
        config.isReservedAttr(hyphenatedKey)) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        )
      }
      defineReactive(props, key, value, () => {
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            `Avoid mutating a prop directly since the value will be ` +
            `overwritten whenever the parent component re-renders. ` +
            `Instead, use a data or computed property based on the prop's ` +
            `value. Prop being mutated: "${key}"`,
            vm
          )
        }
      })
    } else {

      // 为props的每个key设置数据响应式
      defineReactive(props, key, value)
    }
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    if (!(key in vm)) {
      // 将key代理到vm对象上
      proxy(vm, `_props`, key)
    }
  }

  toggleObserving(true)
}

// 处理data对象
function initData(vm: Component) {
  let data = vm.$options.data

  // 判断是一个函数还是一个对象。函数则执行将this指向此实例。没有则默认为空对象
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      // 判断是否和methods中的方法名重名
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    // 判断是否和props中的key相同
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      // 将data绑定至当前实例
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  // 为data对象上的数据设置响应式
  observe(data, true /* asRootData */)
}

export function getData(data: Function, vm: Component): any {
  // #7573 disable dep collection when invoking data getters
  pushTarget()
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, `data()`)
    return {}
  } finally {
    popTarget()
  }
}

const computedWatcherOptions = {lazy: true}

// 处理computed对象
function initComputed(vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()

  for (const key in computed) {
    // 获取key对应的值，即getter函数
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }

    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        // 不能和data中的属性名重名
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        // 不能和props中的属性名重名
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      } else if (vm.$options.methods && key in vm.$options.methods) {
        // 不能和methods中的方法名重名
        warn(`The computed property "${key}" is already defined as a method.`, vm)
      }
    }
  }
}

// 将computed对象中的key代理到target（vm）上
export function defineComputed(
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  if (process.env.NODE_ENV !== 'production' &&
    sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }

  // 对于target.key的访问和设置的拦截
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

// 返回一个函数，此函数在访问vm.computedProperty时会被执行，然后返回执行结果
function createComputedGetter(key) {
  // computed属性会进行缓存
  return function computedGetter() {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // 计算key对应的值，通过执行computed.key的回调函数来得到
      // watcher.dirty 此为computed计算结果会缓存的原理
      // 当在一个组件中使用两次相同的computed时，第一个会执行回调函数计算出实际的值
      // 第二个会执行watcher.evaluate，不走计算过程了
      // 因为上一次执行watcher.evaluate的时候，将watcher.dirty设置成了false
      // 待页面更新之后，才会将watcher.dirty重新设置为true。于是在下次页面更新时，重新计算结果
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

// 功能同上
function createGetterInvoker(fn) {
  return function computedGetter() {
    return fn.call(this, this)
  }
}

/*
* 针对methods
*   1. methods[key] 必须是一个函数
*   2. methods中的可以不能与props中的key相同
*      methods中的key与Vue实例上已有的方法名相同，一般是一些内置方法，$ 和 _开头
*
* */
function initMethods(vm: Component, methods: Object) {
  const props = vm.$options.props
  for (const key in methods) {
    if (process.env.NODE_ENV !== 'production') {
      // 保证methods[key]是一个函数
      if (typeof methods[key] !== 'function') {
        warn(
          `Method "${key}" has type "${typeof methods[key]}" in the component definition. ` +
          `Did you reference the function correctly?`,
          vm
        )
      }

      // 如果方法名在props中有同名key则报错
      if (props && hasOwn(props, key)) {
        warn(
          `Method "${key}" has already been defined as a prop.`,
          vm
        )
      }

      // 如果方法名与内置方法名相同则报错
      if ((key in vm) && isReserved(key)) {
        warn(
          `Method "${key}" conflicts with an existing Vue instance method. ` +
          `Avoid defining component methods that start with _ or $.`
        )
      }
    }
    vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
  }
}

/**
 * 处理 watch 对象的入口，做了两件事：
 *   1、遍历 watch 对象
 *   2、调用 createWatcher 函数
 * @param {*} watch = {
 *   'key1': function(val, oldVal) {},
 *   'key2': 'this.methodName',
 *   'key3': {
 *     handler: function(val, oldVal) {},
 *     deep: true
 *   },
 *   'key4': [
 *     'this.methodNanme',
 *     function handler1() {},
 *     {
 *       handler: function() {},
 *       immediate: true
 *     }
 *   ],
 *   'key.key5' { ... }
 * }
 */
function initWatch(vm: Component, watch: Object) {
  // 遍历watch
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

// 接收实例，监听变量名，函数，操作
function createWatcher(
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  // 判断操作函数是否是一个对象，若为是，则表示是一个配置
  if (isPlainObject(handler)) {
    // 将操作设置为配置项
    options = handler
    // 将操作设置为真实的操作函数
    handler = handler.handler
  }

  // 若handler为字符串，则说明是一个methods方法，获取vm[handler]
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

export function stateMixin(Vue: Class<Component>) {
  // flow somehow has problems with directly declared definition object
  // when using Object.defineProperty, so we have to procedurally build up
  // the object here.
  const dataDef = {}
  dataDef.get = function () {
    return this._data
  }
  const propsDef = {}
  propsDef.get = function () {
    return this._props
  }
  if (process.env.NODE_ENV !== 'production') {
    dataDef.set = function () {
      warn(
        'Avoid replacing instance root $data. ' +
        'Use nested data properties instead.',
        this
      )
    }
    propsDef.set = function () {
      warn(`$props is readonly.`, this)
    }
  }
  Object.defineProperty(Vue.prototype, '$data', dataDef)
  Object.defineProperty(Vue.prototype, '$props', propsDef)

  Vue.prototype.$set = set
  Vue.prototype.$delete = del

  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }

    // 获取配置，若无则设置为空对象
    options = options || {}

    // 标记为用户watch
    options.user = true

    // 创建一个watcher
    const watcher = new Watcher(vm, expOrFn, cb, options)

    // 若用户设置了immediate为true，则立即执行一次回调函数

    options = options || {}
    options.user = true
    if (options.immediate) {
      const info = `callback for immediate watcher "${watcher.expression}"`
      pushTarget()
      invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
      popTarget()
    }

    // 返回unwatch函数，用于解除监听
    return function unwatchFn() {
      watcher.teardown()
    }
  }
}
